﻿using System;
using MbUnit.Framework;

namespace CommonBehavior
{
    /// <summary>
    /// Specifies how the comparison of instances should behave.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [TestFixture]
    abstract public class WhenComparingInstances<T> where T : IComparable<T>
    {
        #region Verifications that implementation of CompareTo(T) is correct

        /// <summary>
        /// A.CompareTo(A) is required to return zero.
        /// </summary>
        [Test]
        public void GivenThatAnInstanceIsComparedToItselfTheResultIsZero()
        {
            // arrange
            T a = this.SmallInstance;

            // act
            int resultOfAComparetoA = a.CompareTo(a);

            // assert
            Assert.AreEqual(0, resultOfAComparetoA);
        }

        /// <summary>
        /// If A.CompareTo(B) returns zero, then B.CompareTo(A) is required to return zero.
        /// </summary>
        [Test]
        public void GivenThatAComparedToBeIsZeroThenBComparedToAShouldBeZero()
        {
            // arrange
            T a = this.SmallInstance;
            T b = this.SmallInstance;

            int resultOfAComparedToB = a.CompareTo(b);
            Assert.AreEqual(0, resultOfAComparedToB);

            // act
            int resultOfBComparedToA = b.CompareTo(a);

            // assert
            Assert.AreEqual(0, resultOfBComparedToA);
        }

        /// <summary>
        /// If A.CompareTo(B) returns zero and B.CompareTo(C) returns zero, then A.CompareTo(C) is required to return zero.
        /// </summary>
        [Test]
        public void GivenThatAComparedToBeIsZeroAndBComparedToCIsZeroThenAComparedToCShouldBeZero()
        {
            // arrange
            T a = this.SmallInstance;
            T b = this.SmallInstance;
            T c = this.SmallInstance;

            int resultOfAComparedToB = a.CompareTo(b);
            Assert.AreEqual(0, resultOfAComparedToB);

            int resultOfBComparedToC = b.CompareTo(c);
            Assert.AreEqual(0, resultOfBComparedToC);

            // act
            int resultOfAComparedToC = a.CompareTo(c);

            // assert
            Assert.AreEqual(0, resultOfAComparedToC);
        }

        /// <summary>
        /// If A.CompareTo(B) returns a value other than zero, then B.CompareTo(A) is required to return a value of the opposite sign.
        /// </summary>
        [Test]
        public void GivenAComparedToBeReturnsAValueOtherThanZeroThenBComparedToAShouldReturnAValueOfTheOppositeSign()
        {
            // arrange
            T a = this.SmallInstance;
            T b = this.MediumInstance;

            int resultOfACompareToB = a.CompareTo(b);
            Assert.AreNotEqual(0, resultOfACompareToB);

            // act
            int resultOfBCompareToA = b.CompareTo(a);

            // assert
            AssertAreOfOppositeSign(resultOfACompareToB, resultOfBCompareToA);
        }

        /// <summary>
        /// If A.CompareTo(B) returns a value x that is not equal to zero, and B.CompareTo(C) returns a value y of the same sign as x, then A.CompareTo(C) is required to return a value of the same sign as x and y. 
        /// </summary>
        [Test]
        public void GivenAComparedToBReturnsAValueXOtherThanZeroAndBComparedToCReturnsAValueYOfTheSameSignAsXThenAComparedToCShouldReturnAValueOfTheSameSignAsXAndY()
        {
            //arrange
            T a = this.SmallInstance;
            T b = this.MediumInstance;
            T c = this.LargeInstance;

            int resultOfAComparedToB = a.CompareTo(b);
            Assert.AreNotEqual(0, resultOfAComparedToB);

            int resultOfBComparedToC = b.CompareTo(c);
            AssertAreOfSameSign(resultOfAComparedToB, resultOfBComparedToC);

            // act
            int resultOfAComparedToC = a.CompareTo(c);

            // assert
            AssertAreOfSameSign(resultOfAComparedToC, resultOfAComparedToB);
            AssertAreOfSameSign(resultOfAComparedToC, resultOfBComparedToC);
        }

        #endregion

        #region Abstract members

        /// <summary>
        /// Returns an instance which is smaller than medium and large instances.
        /// </summary>
        /// <returns></returns>
        protected abstract T SmallInstance { get; }

        /// <summary>
        /// Returns an instance which is larger than small instance and smaller than large instances.
        /// </summary>
        /// <returns></returns>
        protected abstract T MediumInstance { get; }

        /// <summary>
        /// Creates an instance which is larger than small and medium instances.
        /// </summary>
        /// <returns></returns>
        protected abstract T LargeInstance { get; }

        #endregion

        #region Verifications that implementation of XXXInstance is correct

        [Test]
        public void SmallInstanceShouldBeSmallerThanMediumInstance()
        {
            T smallInstance = this.SmallInstance;
            T mediumInstance = this.MediumInstance;

            AssertLessThan(smallInstance, mediumInstance);
        }

        [Test]
        public void SmallInstanceShouldBeSmallerThanLargeInstance()
        {
            T smallInstance = this.SmallInstance;
            T largeInstance = this.LargeInstance;

            AssertLessThan(smallInstance, largeInstance);
        }

        [Test]
        public void MediumInstanceShouldBeSmallerThanLargeInstance()
        {
            T mediumInstance = this.MediumInstance;
            T largeInstance = this.LargeInstance;

            AssertLessThan(mediumInstance, largeInstance);
        }

        [Test]
        public void MediumInstanceShouldBeLargerThanSmallInstance()
        {
            T mediumInstance = this.MediumInstance;
            T smallInstance = this.SmallInstance;

            AssertMoreThan(mediumInstance, smallInstance);
        }

        [Test]
        public void LargeInstanceShouldBeLargerThanSmallInstance()
        {
            T largeInstance = this.LargeInstance;
            T smallInstance = this.SmallInstance;

            AssertMoreThan(largeInstance, smallInstance);
        }

        [Test]
        public void LargeInstanceShouldBeLargerThanMediumInstance()
        {
            T largeInstance = this.LargeInstance;
            T mediumInstance = this.MediumInstance;

            AssertMoreThan(largeInstance, mediumInstance);
        }

        #endregion

        #region Helper methods

        /// <summary>
        /// Asserts that a is strictly less than b.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        private static void AssertLessThan(T a, T b)
        {
            Assert.IsTrue(a.CompareTo(b) < 0);
        }

        /// <summary>
        /// Asserts that a is strictly more than b.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        private static void AssertMoreThan(T a, T b)
        {
            Assert.IsTrue(a.CompareTo(b) > 0);
        }

        /// <summary>
        /// Asserts that two integers have the same sign.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        private static void AssertAreOfSameSign(int a, int b)
        {
            Assert.IsTrue(AreOfSameSign(a, b));
        }

        /// <summary>
        /// Asserts that two integers have opposite signs.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        private static void AssertAreOfOppositeSign(int a, int b)
        {
            Assert.IsFalse(AreOfSameSign(a, b));
        }

        /// <summary>
        /// Determines if two integers have the same sign.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns>True when they have the same sign, false otherwise.</returns>
        private static bool AreOfSameSign(int a, int b)
        {
            if (a < 0 && b < 0) return true;
            if (a > 0 && b > 0) return true;
            return false;
        }

        #endregion
    }
}
